﻿/*              Copyright(c) 2008 Dave Sexton              *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using DaveSexton.DocToMaml.Extensions;

namespace DaveSexton.DocToMaml.Maml
{
	internal sealed class DeveloperConceptualDocument : MamlDocumentBase
	{
		#region Public Properties
		public override string DefaultContainerElementName
		{
			get
			{
				return "para";
			}
		}
		#endregion

		#region Private / Protected
		private readonly List<KeyValuePair<string, string>?> sections = new List<KeyValuePair<string, string>?>();
		private readonly List<MamlLink> relatedTopics = new List<MamlLink>();
		private string summary, introduction;
		private bool sectionContentParsedBeforeTitle;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="DeveloperConceptualDocument" /> class.
		/// </summary>
		public DeveloperConceptualDocument(IRuleContainer rules, ConceptualImageManager imageManager, FileGuidManager guidManager)
			: base(rules, imageManager, guidManager)
		{
		}
		#endregion

		#region Methods
		public override void Reset()
		{
			sections.Clear();
			relatedTopics.Clear();
			summary = null;
			introduction = null;
			sectionContentParsedBeforeTitle = false;

			base.Reset();
		}

		protected override void CleanMamlDocument(XmlReader reader, XmlWriter writer)
		{
			CleanMamlElementsRecursive(reader, writer, false);
		}

		private bool CleanMamlElementsRecursive(XmlReader reader, XmlWriter writer, bool cleaningStructure)
		{
			reader.MoveToContent();

			bool hasContent = false;

			while (!reader.EOF)
			{
				switch (reader.NodeType)
				{
					case XmlNodeType.Element:
						if (cleaningStructure)
						// write the structure's start element but do not set hasContent to true
						{
							writer.WriteStartElement(reader);
							cleaningStructure = false;
						}
						else if (CleanMamlElement(reader, writer))
							hasContent = true;
						break;
					case XmlNodeType.EndElement:
						writer.WriteEndElement();
						reader.Read();
						break;
					default:
						writer.WriteNode(reader, false);
						hasContent = true;
						break;
				}
			}

			return hasContent;
		}

		private bool CleanMamlElement(XmlReader reader, XmlWriter writer)
		{
			if (reader.IsEmptyElement && reader.AttributeCount == 0)
				return CleanMamlElementWithoutContent(reader, writer);
			else
				return CleanMamlElementWithContent(reader, writer);
		}

		private bool CleanMamlElementWithContent(XmlReader reader, XmlWriter writer)
		{
			bool hasContent;

			switch (reader.Name)
			{
				case "section":
					hasContent = CleanMamlStructure(reader, writer);
					reader.Read();
					return hasContent;
				case "list":
					hasContent = CleanMamlStructure(reader, writer);
					reader.Read();
					return hasContent;
				case "table":
					hasContent = CleanMamlStructure(reader, writer);
					reader.Read();
					return hasContent;
				case "introduction":
				case "content":						// required by section elements
				case "entry":							// required by table elements
				case "title":							// required by section and table elements
				case "listItem":					// required by list elements
					writer.WriteStartElement(reader);

					// the caller should not keep an entire structure when it only contains required elements if they contain only empty elements.
					// Therefore, the caller doen't need to know that these elements have been written.  If child elements exist and they have 
					// content then the caller will discover that later.
					return false;
				default:
					writer.WriteStartElement(reader);
					return true;
			}
		}

		private bool CleanMamlElementWithoutContent(XmlReader reader, XmlWriter writer)
		{
			switch (reader.Name)
			// Required elements must be written, even if they are empty.
			{
				case "relatedTopics":
					// a child container is not required.
					writer.WriteStartElement(reader);
					return true;
				case "introduction":
					// a child container is requried, even if it's empty
					writer.WriteStartElement(reader.Name);
					writer.WriteStartElement(DefaultContainerElementName);
					writer.WriteEndElement();
					writer.WriteEndElement();

					reader.Read();
					return true;
				case "content":						// required by section elements
				case "entry":							// required by table elements
					// a child container is requried, even if it's empty
					writer.WriteStartElement(reader.Name);
					writer.WriteStartElement(DefaultContainerElementName);
					writer.WriteEndElement();
					writer.WriteEndElement();

					reader.Read();
					return false;
				case "title":							// required by section and table elements
				case "listItem":					// required by list elements
					// a child container is not required by these elements.
					writer.WriteStartElement(reader);

					// the caller should not keep an entire structure when it only contains required elements that are empty.
					// Therefore, the caller doen't need to know that these elements have been written.
					return false;
				default:
					// skip this empty element.
					reader.Read();
					return false;
			}
		}

		private bool CleanMamlStructure(XmlReader reader, XmlWriter writer)
		{
			using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
			{
				bool hasContent;

				using (XmlWriter memoryWriter = XmlWriter.Create(stream))
				{
					using (XmlReader subtree = reader.ReadSubtree())
					{
						hasContent = CleanMamlElementsRecursive(subtree, memoryWriter, true);
					}
				}

				if (hasContent)
				{
					stream.Position = 0;

					using (XmlReader streamReader = XmlReader.Create(stream, reader.Settings))
					{
						streamReader.MoveToContent();

						// NOTE: writer.WriteNode(streamReader, false) cannot be used here since it will write all namespace declarations
						// in streamReader, even if they're already defined on the root element.  See this discussion: 
						// http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=3161633&SiteID=1
						// TODO: Use XLinq instead (although the project must then target .NET 3.5 instead of 2.0).
						writer.WriteNode(streamReader);
					}

					return true;
				}
				else
					return false;
			}
		}

		protected override void WriteMamlDocument(XmlWriter writer, Guid guid)
		{
			writer.WriteStartDocument();
			writer.WriteStartElement("topic");
			writer.WriteAttributeString("id", guid.ToString());
			writer.WriteAttributeString("revisionNumber", "0");
			writer.WriteStartElement("developerConceptualDocument", "http://ddue.schemas.microsoft.com/authoring/2003/5");
			writer.WriteAttributeString("xmlns", "xlink", null, "http://www.w3.org/1999/xlink");

			if (!string.IsNullOrEmpty(summary))
			{
				writer.WriteStartElement("summary");
				writer.WriteRaw(summary);
				writer.WriteEndElement();
			}

			writer.WriteStartElement("introduction");
			writer.WriteRaw(introduction);
			writer.WriteEndElement();

			foreach (KeyValuePair<string, string>? section in sections)
			{
				if (section != null)
					WriteMamlSection(section.Value.Key, section.Value.Value, writer);
			}

			writer.WriteStartElement("relatedTopics");

			foreach (MamlLink link in relatedTopics)
				link.Write(writer);

			writer.WriteEndElement();

			writer.WriteEndElement();
			writer.WriteEndDocument();
		}

		private static void WriteMamlSection(string title, string contents, XmlWriter writer)
		{
			writer.WriteStartElement("section");
			writer.WriteElementString("title", title);
			writer.WriteStartElement("content");
			writer.WriteRaw(contents);
			writer.WriteEndElement();
			writer.WriteEndElement();
		}

		protected override bool ParseHtmlElementIntoMamlElement(XmlReader reader, MamlElementType element, bool inOuterContext)
		{
			switch (element)
			{
				case MamlElementType.Summary:
					if (string.IsNullOrEmpty(summary))
						summary = ReadAsMamlContent(reader, true);
					else
						summary += Environment.NewLine + ReadAsMamlContent(reader, true);
					break;
				case MamlElementType.Introduction:
					if (string.IsNullOrEmpty(introduction))
						introduction = ReadAsMamlContent(reader, true);
					else
						introduction += Environment.NewLine + ReadAsMamlContent(reader, true);
					break;
				case MamlElementType.Section:
					sections.Add(null);
					reader.Read();
					break;
				case MamlElementType.SectionTitle:
					ParseSectionTitle(reader);
					break;
				case MamlElementType.SectionContent:
					ParseSectionContent(reader);
					break;
				case MamlElementType.RelatedTopics:
					ParseRelatedTopics(reader);
					break;
				default:
					if (!inOuterContext)
						// Since we're already in a section, the element must not be parsed here.  Instead, the caller must write it to the current section.
						return false;
					else
						ParseSectionContent(reader);
					break;
			}

			return true;
		}

		private void ParseSectionTitle(XmlReader reader)
		{
			KeyValuePair<string, string>? section;
			bool addNew = false;

			if (sections.Count == 0 || (section = sections[sections.Count - 1]) == null)
				section = new KeyValuePair<string, string>(ReadElementAsString(reader, true), string.Empty);
			else if (string.IsNullOrEmpty(section.Value.Value))
				// The current section has no content so concatenate this title with the current title.
				section = new KeyValuePair<string, string>(section.Value.Key + " " + ReadElementAsString(reader, true), string.Empty);
			else
			// A title must be parsed before content in the same section; therefore, the content must always be string.Empty since a new section is always being created.
			{
				section = new KeyValuePair<string, string>(ReadElementAsString(reader, true), string.Empty);
				addNew = true;
			}

			if (addNew || sections.Count == 0 || sectionContentParsedBeforeTitle)
			{
				sections.Add(section);
				sectionContentParsedBeforeTitle = false;
			}
			else
				// the parsed title has been added to the current section - save it
				sections[sections.Count - 1] = section;
		}

		private void ParseSectionContent(XmlReader reader)
		{
			KeyValuePair<string, string>? section;

			if (sections.Count == 0)
			{
				string content = ReadAsMamlContent(reader, true);

				if (sections.Count > 0)
					// a default section was already added while parsing content
					return;
				else
				{
					section = new KeyValuePair<string, string>(string.Empty, content);
					sectionContentParsedBeforeTitle = true;
				}
			}
			else if (sections.Count == 0 || ((section = sections[sections.Count - 1]) == null))
			{
				int previousCount = sections.Count;

				string content = ReadAsMamlContent(reader, true);

				if (sections.Count > previousCount)
					// a new section was already added while parsing content
					return;
				else
				{
					section = new KeyValuePair<string, string>(string.Empty, content);
					sectionContentParsedBeforeTitle = true;
				}
			}
			else
			{
				string title = section.Value.Key;
				string content = section.Value.Value;
				section = new KeyValuePair<string, string>(title, content + Environment.NewLine + ReadAsMamlContent(reader, true));
				sectionContentParsedBeforeTitle = string.IsNullOrEmpty(title);
			}

			if (sections.Count == 0)
				sections.Add(section);
			else
				sections[sections.Count - 1] = section;
		}

		private void ParseRelatedTopics(XmlReader reader)
		{
			using (XmlReader subtree = reader.ReadSubtree())
			{
				while (subtree.Read())
				{
					if (subtree.NodeType == XmlNodeType.Element && string.Equals(subtree.Name, "a", StringComparison.Ordinal))
					{
						relatedTopics.Add(ReadHtmlAnchor(subtree));
					}
				}
			}
		}
		#endregion
	}
}
